﻿namespace NTLib.Core.Configuration.Attributes
{
    using System;
    using System.Reflection;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.IO;
    using Serialization;
    using Exceptions.Configuration;
    using System.Threading;

    /// <summary>
    /// 
    /// </summary>
    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
    public sealed class EmbeddedConfigurationAttribute : BaseConfigurationAttribute
    {
        #region Fields

        private static Type s_listGenericType;

        #endregion

        #region Properties

        /// <summary>
        /// Gets or sets a type in the host assembly where the resource file is embbeded.
        /// </summary>
        /// <value>
        /// The type in the host assembly.
        /// </value>
        public Type TypeInTheHostAssembly { get; set; }

        /// <summary>
        /// Gets or sets the resource file path.
        /// </summary>
        /// <value>
        /// The resource file path.
        /// </value>
        public string ResourceFilePath { get; set; }

        /// <summary>
        /// Gets or sets the type of the serialized.
        /// </summary>
        /// <value>
        /// The type of the serialized.
        /// </value>
        public Type ConfigValueType { get; set; }

        /// <summary>
        /// Gets or sets the type of the serializor.
        /// </summary>
        /// <value>
        /// The type of the serializor.
        /// </value>
        public Type SerializerType { get; set; }

        /// <summary>
        /// Gets or sets a value indicating whether a throw must occure if the file is not found.
        /// </summary>
        /// <value>
        /// <c>true</c> if a throw must occure if the file is not found; otherwise, <c>false</c>.
        /// </value>
        public bool ThrowIfFileNotFound { get; set; }

        #endregion

        #region Ctor

        /// <summary>
        /// Initializes the <see cref="EmbeddedConfigurationAttribute"/> class.
        /// </summary>
        static EmbeddedConfigurationAttribute()
        {
            s_listGenericType = typeof(List<>);
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="EmbeddedConfigurationAttribute"/> class.
        /// </summary>
        public EmbeddedConfigurationAttribute()
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="EmbeddedConfigurationAttribute" /> class.
        /// </summary>
        /// <param name="typeInTheHostAssembly">The type in the host assembly.</param>
        /// <param name="resourceFilePath">The resource file path.</param>
        /// <param name="configValueType">Type of the configuration value.</param>
        /// <param name="serializorType">Type of the serializor.</param>
        public EmbeddedConfigurationAttribute(Type typeInTheHostAssembly, string resourceFilePath, Type configValueType, Type serializorType)
        {
            this.TypeInTheHostAssembly = typeInTheHostAssembly;
            this.ResourceFilePath = resourceFilePath;
            this.ConfigValueType = configValueType;
            this.SerializerType = serializorType;
        }

        #endregion

        #region Methods

        /// <summary>
        /// Collects the configuration value.
        /// </summary>
        public override ConfigurationCollection CollectConfigurationValue(Type host, object context = null, CancellationToken token = default(CancellationToken))
        {
            if (this.TypeInTheHostAssembly == null)
                throw new ArgumentNullException(nameof(this.TypeInTheHostAssembly));

            if (this.ResourceFilePath == null)
                throw new ArgumentNullException(nameof(this.ResourceFilePath));

            if (this.ConfigValueType == null)
                throw new ArgumentNullException(nameof(this.ConfigValueType));

            if (this.SerializerType == null)
                throw new ArgumentNullException(nameof(this.SerializerType));

            Assembly assembly = null;

            if (context != null && context is Assembly)
                assembly = context as Assembly;
            else
                assembly = this.TypeInTheHostAssembly.GetTypeInfo().Assembly;
            Stream stream = null;
            try
            {
                stream = assembly.GetManifestResourceStream(this.ResourceFilePath);
            }
            catch (Exception)
            {
                if (ThrowIfFileNotFound)
                    throw;
                return new ConfigurationCollection();
            }
            using (stream)
            {
                token.ThrowIfCancellationRequested();
                if (stream == null)
                    throw new ConfigurationResourceNotFoundException(string.Format(Strings.ExceptionResources.ConfigurationResourcesNotFoundException, this.ResourceFilePath));

                ISerializor serializer = null;
                try
                {
                    serializer = Activator.CreateInstance(this.SerializerType) as ISerializor;
                    if (serializer == null)
                        throw new BadParameterConfigurationException(nameof(SerializerType), typeof(ISerializor).ToString());
                    token.ThrowIfCancellationRequested();
                }
                catch (Exception ex)
                {
                    throw new ConfigurationFailedException(string.Format(Strings.ExceptionResources.SerializerDoesntExistException, this.SerializerType), ex);
                }

                Type loadingType = s_listGenericType.MakeGenericType(this.ConfigValueType);
                try
                {
                    token.ThrowIfCancellationRequested();
                    var dataCollection = serializer.ReadObject(loadingType, stream);

                    token.ThrowIfCancellationRequested();
                    // if (dataCollection != null && serializer != null) : Not check to trigger an execption if the value are invalid
                    ConfigurationCollection collection = new ConfigurationCollection();

                    foreach (var data in dataCollection as IEnumerable<ConfigValue>)
                    {
                        token.ThrowIfCancellationRequested();

                        List<ConfigValue> values = null;
                        if (collection.TryGetValue(data.ConfigKey, out values))
                        {
                            values.Add(data);
                        }
                        else
                        {
                            collection.Add(data.ConfigKey, new List<ConfigValue>() { data });
                        }
                    }

                    return collection;
                }
                catch (Exception ex)
                {
                    throw new ConfigurationFailedException(string.Format(Strings.ExceptionResources.DeserialisationFailedException, loadingType), ex);
                }
            }
            throw new InvalidOperationException();
        }

        #endregion
    }
}
